﻿using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Web.Razor.Parser.SyntaxTree;
using Microsoft.Internal.Web.Utils;

namespace System.Web.Razor.Text {
    public struct TextChange {
        private string _newText;
        private string _oldText;

        public int OldPosition { get; private set; }
        public int NewPosition { get; private set; }
        public int OldLength { get; private set; }
        public int NewLength { get; private set; }
        public ITextBuffer NewBuffer { get; private set; }
        public ITextBuffer OldBuffer { get; private set; }

        public string OldText {
            get {
                if (_oldText == null && OldBuffer != null) {
                    _oldText = GetText(OldBuffer, OldPosition, OldLength);
                }
                return _oldText;
            }
        }

        public string NewText {
            get {
                if (_newText == null) {
                    _newText = GetText(NewBuffer, NewPosition, NewLength);
                }
                return _newText;
            }
        }

        public bool IsInsert {
            get {
                return OldLength == 0 && NewLength > 0;
            }
        }

        public bool IsDelete {
            get {
                return OldLength > 0 && NewLength == 0;
            }
        }

        public bool IsReplace {
            get {
                return OldLength > 0 && NewLength > 0;
            }
        }

        // Constructor for changes where the position hasn't moved (primarily for tests)
        internal TextChange(int position, int oldLength, ITextBuffer oldBuffer, int newLength, ITextBuffer newBuffer)
            : this(position, oldLength, oldBuffer, position, newLength, newBuffer) {
        }

        public TextChange(int oldPosition, int oldLength, ITextBuffer oldBuffer, int newPosition, int newLength, ITextBuffer newBuffer)
            : this() {
            if (oldPosition < 0) { throw new ArgumentOutOfRangeException("oldPosition", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0")); }
            if (newPosition < 0) { throw new ArgumentOutOfRangeException("newPosition", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0")); }
            if (oldLength < 0) { throw new ArgumentOutOfRangeException("oldLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0")); }
            if (newLength < 0) { throw new ArgumentOutOfRangeException("newLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0")); }
            if (oldBuffer == null) { throw new ArgumentNullException("oldBuffer"); }
            if (newBuffer == null) { throw new ArgumentNullException("newBuffer"); }

            OldPosition = oldPosition;
            NewPosition = newPosition;
            OldLength = oldLength;
            NewLength = newLength;
            NewBuffer = newBuffer;
            OldBuffer = oldBuffer;
        }

        public override bool Equals(object obj) {
            if (!(obj is TextChange)) {
                return false;
            }
            TextChange change = (TextChange)obj;
            return (change.OldPosition == OldPosition) &&
                   (change.NewPosition == NewPosition) &&
                   (change.OldLength == OldLength) &&
                   (change.NewLength == NewLength) &&
                   OldBuffer.Equals(change.OldBuffer) &&
                   NewBuffer.Equals(change.NewBuffer);
        }

        public string ApplyChange(string content, int changeOffset) {
            int changeRelativePosition = OldPosition - changeOffset;

            Debug.Assert(changeRelativePosition >= 0);
            return content.Remove(changeRelativePosition, OldLength)
                          .Insert(changeRelativePosition, NewText);
        }

        /// <summary>
        /// Applies the text change to the content of the span and returns the new content.
        /// This method doesn't update the span content.
        /// </summary>
        public string ApplyChange(Span span) {
            return ApplyChange(span.Content, span.Start.AbsoluteIndex);
        }

        public override int GetHashCode() {
            return OldPosition ^ NewPosition ^ OldLength ^ NewLength ^ NewBuffer.GetHashCode() ^ OldBuffer.GetHashCode();
        }

        public static bool operator ==(TextChange left, TextChange right) {
            return left.Equals(right);
        }

        public static bool operator !=(TextChange left, TextChange right) {
            return !left.Equals(right);
        }

        public override string ToString() {
            return String.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText);
        }

        /// <summary>
        /// Removes a common prefix from the edit to turn IntelliSense replacements into insertions where possible
        /// </summary>
        /// <returns>A normalized text change</returns>
        public TextChange Normalize() {
            if (OldBuffer != null && IsReplace && NewLength > OldLength && NewText.StartsWith(OldText, StringComparison.Ordinal) && NewPosition == OldPosition) {
                // Normalize the change into an insertion of the uncommon suffix (i.e. strip out the common prefix)
                return new TextChange(oldPosition: OldPosition + OldLength,
                                      oldLength: 0,
                                      oldBuffer: OldBuffer,
                                      newPosition: OldPosition + OldLength,
                                      newLength: NewLength - OldLength,
                                      newBuffer: NewBuffer);
            }
            return this;
        }

        private string GetText(ITextBuffer buffer, int position, int length) {
            int oldPosition = buffer.Position;
            try {
                buffer.Position = position;
                // Optimization for the common case of one char inserts
                if (NewLength == 1) {
                    return ((char)buffer.Read()).ToString();
                }
                else {
                    var builder = new StringBuilder();
                    for (int i = 0; i < length; i++) {
                        char c = (char)buffer.Read();
                        builder.Append(c);
                        if (Char.IsHighSurrogate(c)) {
                            builder.Append((char)buffer.Read());
                        }
                    }
                    return builder.ToString();
                }
            }
            finally {
                buffer.Position = oldPosition;
            }
        }
    }
}
